<?php
/*
 * Limb PHP Framework
 *
 * @link http://limb-project.com
 * @copyright  Copyright &copy; 2004-2009 BIT(http://bit-creative.com)
 * @license    LGPL http://www.gnu.org/copyleft/lesser.html
 */

namespace limb\datetime\src;

use limb\core\src\lmbObject;
use limb\core\src\exception\lmbException;

/**
 * class lmbDateTime.
 *
 * @package datetime
 * @version $Id: lmbDateTime.php 6532 2007-11-20 15:55:48Z
 */
class lmbDateTime extends lmbObject
{
    const MINUTE = 60;
    const HOUR = 3600;
    const DAY = 86400;
    const WEEK = 604800;

    //YYYY-MM-DD HH:MM:SS timezone
    const DATE_ISO_REGEX = '~^(([0-9]{4})-([0-9]{2})-([0-9]{2}))?((?(1)\s+)([0-9]{2}):([0-9]{2}):?([0-9]{2})?)?$~';

    /**
     * Defines what day starts the week.
     * Monday (1) is the international standard, Sunday (0) is used in US.
     * @see setWeekStartsAt()
     */
    static protected $week_starts_at = 1;

    protected $year = 0;
    protected $month = 0;
    protected $day = 0;
    protected $hour = 0;
    protected $minute = 0;
    protected $second = 0;
    protected $tz = '';

    function __construct($year_or_date = null, $month_or_tz = null, $day = null, $hour = 0, $minute = 0, $second = 0, $tz = '')
    {
        if (func_num_args() > 2) {
            $this->year = (int)$year_or_date;
            $this->month = (int)$month_or_tz;
            $this->day = (int)$day;
            $this->hour = (int)$hour;
            $this->minute = (int)$minute;
            $this->second = (int)$second;
            $this->tz = $tz;
        } elseif ($year_or_date instanceof lmbDateTime) {
            $this->_copy($year_or_date);
        } elseif (is_numeric($year_or_date)) {
            $this->_setByStamp($year_or_date);
            $this->tz = $month_or_tz;
        } elseif (is_string($year_or_date)) {
            $this->_setByString($year_or_date);
            $this->tz = $month_or_tz;
        } else {
            $this->_setByStamp(time());
            $this->tz = $month_or_tz;
        }

        if (!$this->isValid()) {
            $args = func_get_args();
            throw new lmbException("Could not create date using args", $args);
        }
    }

    /**
     * Wrapper around constructor
     *
     * It can be useful since the following is not allowed in PHP 'new lmbDateTime(..)->addDay(..)->'
     *
     * @param integer $year_or_date
     * @param integer $month_or_tz
     * @param integer $day
     * @param integer $hour
     * @param integer $minute
     * @param integer $second
     * @param string $tz
     * @return lmbDateTime
     */
    static function create($year_or_date = null, $month_or_tz = null, $day = null, $hour = 0, $minute = 0, $second = 0, $tz = '')
    {
        if (func_num_args() > 2)
            return new self($year_or_date, $month_or_tz, $day, $hour, $minute, $second, $tz);
        else
            return new self($year_or_date, $month_or_tz);
    }

    static function createByDays($days)
    {
        $days -= 1721119;
        $century = floor((4 * $days - 1) / 146097);
        $days = floor(4 * $days - 1 - 146097 * $century);
        $day = floor($days / 4);

        $year = floor((4 * $day + 3) / 1461);
        $day = floor(4 * $day + 3 - 1461 * $year);
        $day = floor(($day + 4) / 4);

        $month = floor((5 * $day - 3) / 153);
        $day = floor(5 * $day - 3 - 153 * $month);
        $day = floor(($day + 5) / 5);

        if ($month < 10) {
            $month += 3;
        } else {
            $month -= 9;
            if ($year++ == 99) {
                $year = 0;
                $century++;
            }
        }

        $century = sprintf('%02d', $century);
        $year = sprintf('%02d', $year);
        return new self($century . $year, $month, $day);
    }

    static function setWeekStartsAt($n)
    {
        self::$week_starts_at = $n;
    }

    static function getWeekStartsAt()
    {
        return self::$week_starts_at;
    }

    static function stampToIso($stamp)
    {
        $date = new self((int)$stamp);
        return $date->getIsoDate();
    }

    static function stampToShortIso($stamp)
    {
        $date = new self((int)$stamp);
        return $date->getIsoShortDate();
    }

    function _createTimeZoneObject($code = null)
    {
        if (!$code)
            return lmbDateTimeZone::getDefault();
        else
            return new lmbDateTimeZone($code);
    }

    function isValid()
    {
        if ($this->year < 0) return false;
        if ($this->month < 0 || $this->month > 12) return false;
        if ($this->day < 0 || $this->day > 31) return false;
        if ($this->hour < 0 || $this->hour > 23) return false;
        if ($this->minute < 0 || $this->minute > 59) return false;
        if ($this->second < 0 || $this->second > 59) return false;

        //dirty hack for checkdate...
        return checkdate($this->month ?: 1,
            $this->day ?: 1,
            $this->year ?: 1);
    }

    static function validate($year_or_date = null, $month_or_tz = null, $day = null, $hour = 0, $minute = 0, $second = 0, $tz = '')
    {
        try {
            if (func_num_args() > 2)
                new self($year_or_date, $month_or_tz, $day, $hour, $minute, $second, $tz);
            else
                new self($year_or_date, $month_or_tz);
            return true;
        } catch (lmbException $e) {
            return false;
        }
    }

    protected function _setByString($string)
    {
        if (!preg_match(self::DATE_ISO_REGEX, trim($string), $regs))
            throw new lmbException("Could not setup date using string '$string'");

        if (isset($regs[1])) {
            $this->year = (int)$regs[2];
            $this->month = (int)$regs[3];
            $this->day = (int)$regs[4];
        }

        if (isset($regs[5])) {
            $this->hour = (int)$regs[6];
            $this->minute = (int)$regs[7];
            if (isset($regs[8]))
                $this->second = (int)$regs[8];
        }
    }

    protected function _setByStamp($time)
    {
        if (!$arr = @getdate($time))
            throw new lmbException("Could not setup date using stamp'$time'");

        $this->year = $arr['year'];
        $this->month = $arr['mon'];
        $this->day = $arr['mday'];
        $this->hour = $arr['hours'];
        $this->minute = $arr['minutes'];
        $this->second = $arr['seconds'];
    }

    protected function _copy($date)
    {
        $this->year = $date->getYear();
        $this->month = $date->getMonth();
        $this->day = $date->getDay();
        $this->hour = $date->getHour();
        $this->minute = $date->getMinute();
        $this->second = $date->getSecond();
        $this->tz = $date->getTimeZone();
    }

    function getStamp()
    {
        //temporary ugly hack for unspecified year
        if (!$this->year)
            return mktime($this->hour, $this->minute, $this->second, $this->month, $this->day);
        else
            return mktime($this->hour, $this->minute, $this->second, $this->month, $this->day, $this->year);
    }

    function setStamp($time)
    {
        $this->_setByStamp($time);
    }

    function date($format): string
    {
        return date($format, $this->getStamp());
    }

    function format($format): string
    {
        return (new \DateTime())->setTimestamp($this->getStamp())->format($format);
    }

    function strftime($format): string
    {
        return $this->format($format);
    }

    function getIsoDate($with_seconds = true)
    {
        return sprintf($with_seconds ? '%04d-%02d-%02d %02d:%02d:%02d' : '%04d-%02d-%02d %02d:%02d',
            $this->getYear(), $this->getMonth(), $this->getDay(),
            $this->getHour(), $this->getMinute(), $this->getSecond());
    }

    function getIsoShortDate()
    {
        return sprintf('%04d-%02d-%02d',
            $this->getYear(), $this->getMonth(), $this->getDay());
    }

    function getIsoTime($with_seconds = true)
    {
        return sprintf($with_seconds ? '%02d:%02d:%02d' : '%02d:%02d',
            $this->getHour(), $this->getMinute(), $this->getSecond());
    }

    function getIsoShortTime()
    {
        return $this->getIsoTime(false);
    }

    function toString()
    {
        return $this->getIsoDate();
    }

    function isInDaylightTime()
    {
        return $this->getTimeZoneObject()->inDaylightTime($this);
    }

    function toUTC()
    {
        $tz = $this->getTimeZoneObject();

        if ($tz->getOffset($this) > 0)
            $date = $this->addSecond(-1 * intval($tz->getOffset($this) / 1000));
        else
            $date = $this->addSecond(intval(abs($tz->getOffset($this)) / 1000));

        return $date->withTimeZone('UTC');
    }

    /**
     * Compares object with $d date object.
     * return int 0 if the dates are equal, -1 if is before, 1 if is after than $d
     */
    function compare($d): int
    {
        if (!$d instanceof lmbDateTime)
            throw new lmbException("Wrong date argument", array('arg' => $d));

        $s1 = $this->getStamp();
        $s2 = $d->getStamp();

        if ($s1 > $s2)
            return 1;
        elseif ($s2 > $s1)
            return -1;
        else
            return 0;
    }

    function isBefore($when, $use_time_zone = false): bool
    {
        if ($this->compare($when, $use_time_zone) == -1)
            return true;
        else
            return false;
    }

    function isAfter($when, $use_time_zone = false): bool
    {
        if ($this->compare($when, $use_time_zone) == 1)
            return true;
        else
            return false;
    }

    function isEqual($when, $use_time_zone = false)
    {
        if ($this->compare($when, $use_time_zone) == 0)
            return true;
        else
            return false;
    }

    function isEqualDate($when)
    {
        return $this->stripTime()->isEqual($when->stripTime());
    }

    function isLeapYear()
    {
        return (($this->year % 4 == 0 && $this->year % 100 != 0) || $this->year % 400 == 0);
    }

    function getDayOfYear()
    {
        $days = array(0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334);

        $julian = ($days[$this->month - 1] + $this->day);

        if ($this->month > 2 && $this->isLeapYear())
            $julian++;

        return $julian;
    }

    function getDayOfWeek()
    {
        return $this->_correctDayOfWeek($this->getPhpDayOfWeek(), self::$week_starts_at);
    }

    function getIntlDayOfWeek()
    {
        return $this->_correctDayOfWeek($this->getPhpDayOfWeek(), 1);
    }

    function getPhpDayOfWeek()
    {
        $year = $this->year;
        $month = $this->month;
        $day = $this->day;

        if (1901 < $year && $year < 2038)
            return (int)date('w', mktime(0, 0, 0, $month, $day, $year));

        if ($month > 2) {
            $month -= 2;
        } else {
            $month += 10;
            $year--;
        }

        $day = (floor((13 * $month - 1) / 5) +
            $day + ($year % 100) +
            floor(($year % 100) / 4) +
            floor(($year / 100) / 4) - 2 *
            floor($year / 100) + 77);

        return $day - 7 * floor($day / 7);
    }

    protected function _correctDayOfWeek($dow, $week_starts_at)
    {
        if ($week_starts_at == 0)
            return $dow;

        if ($dow == 0)
            return 6;
        return $dow - 1;
    }

    function getBeginOfDay()
    {
        $class = get_class($this);
        return new $class($this->year, $this->month, $this->day, $this->tz);
    }

    function getEndOfDay()
    {
        $class = get_class($this);
        return new $class($this->year, $this->month, $this->day, 23, 59, 59, $this->tz);
    }

    function getBeginOfWeek()
    {
        $this_weekday = $this->getPhpDayOfWeek();
        $interval = (7 - self::$week_starts_at + $this_weekday) % 7;
        return self::createByDays($this->getDateDays() - $interval);
    }

    function getEndOfWeek()
    {
        $this_weekday = $this->getPhpDayOfWeek();
        $interval = (6 + self::$week_starts_at - $this_weekday) % 7;
        return self::createByDays($this->getDateDays() + $interval)->getEndOfDay();
    }

    function getBeginOfMonth()
    {
        $class = get_class($this);
        return new $class($this->year, $this->month, 1, $this->tz);
    }

    function getEndOfMonth()
    {
        return $this->setDay(1)->addMonth(1)->addDay(-1)->getEndOfDay();
    }

    function getBeginOfYear()
    {
        $class = get_class($this);
        return new $class($this->year, 1, 1, $this->tz);
    }

    function getEndOfYear()
    {
        $class = get_class($this);
        return new $class($this->year, 12, 31, 23, 59, 59, $this->tz);
    }

    function getWeekOfYear()
    {
        $day = $this->day;
        $month = $this->month;
        $year = $this->year;

        if ((1901 < $year) && ($year < 2038)) {
            $res = (int)date('W', mktime(0, 0, 0, $month, $day, $year));
            if ($res > 52) //bug in PHP date???
                return $res % 52;
            return $res;
        }

        $dayofweek = $this->getPhpDayOfWeek();
        $firstday = self::create($year, 1, 1)->getPhpDayOfWeek();
        if (($month == 1) && (($firstday < 1) || ($firstday > 4)) && ($day < 4)) {
            $firstday = self::create($year - 1, 1, 1)->getPhpDayOfWeek();
            $month = 12;
            $day = 31;
        } elseif (($month == 12) && ((self::create($year + 1, 1, 1)->getPhpDayOfWeek() < 5) &&
                (self::create($year + 1, 1, 1)->getPhpDayOfWeek() > 0)))
            return 1;

        return intval(((self::create($year, 1, 1)->getPhpDayOfWeek() < 5) && (self::create($year, 1, 1)->getPhpDayOfWeek() > 0)) +
            4 * ($month - 1) + (2 * ($month - 1) + ($day - 1) + $firstday - $dayofweek + 6) * 36 / 256);
    }

    function getDateDays()
    {
        $century = (int)substr("{$this->year}", 0, 2);
        $year = (int)substr("{$this->year}", 2, 2);
        $month = $this->month;
        $day = $this->day;

        if ($month > 2)
            $month -= 3;
        else {
            $month += 9;
            if ($year)
                $year--;
            else {
                $year = 99;
                $century--;
            }
        }
        return (
            floor((146097 * $century) / 4) +
            floor((1461 * $year) / 4) +
            floor((153 * $month + 2) / 5) +
            $day + 1721119);
    }

    function getYear()
    {
        return $this->year;
    }

    function getMonth()
    {
        return $this->month;
    }

    function getDay()
    {
        return $this->day;
    }

    function getHour()
    {
        return $this->hour;
    }

    function getMinute()
    {
        return $this->minute;
    }

    function getSecond()
    {
        return $this->second;
    }

    function getTimeZoneObject()
    {
        return $this->_createTimeZoneObject($this->tz);
    }

    function getTimeZone()
    {
        return $this->tz;
    }

    function setYear($y)
    {
        $class = get_class($this);
        return new $class($y, $this->month, $this->day, $this->hour, $this->minute, $this->second, $this->tz);
    }

    function setMonth($m)
    {
        $class = get_class($this);
        return new $class($this->year, $m, $this->day, $this->hour, $this->minute, $this->second, $this->tz);
    }

    function setDay($d)
    {
        $class = get_class($this);
        return new $class($this->year, $this->month, $d, $this->hour, $this->minute, $this->second, $this->tz);
    }

    function setHour($h)
    {
        $class = get_class($this);
        return new $class($this->year, $this->month, $this->day, $h, $this->minute, $this->second, $this->tz);
    }

    function setMinute($m)
    {
        $class = get_class($this);
        return new $class($this->year, $this->month, $this->day, $this->hour, $m, $this->second, $this->tz);
    }

    function setSecond($s)
    {
        $class = get_class($this);
        return new $class($this->year, $this->month, $this->day, $this->hour, $this->minute, $s, $this->tz);
    }

    function setTimeZone($tz)
    {
        return $this->withTimeZone($tz);
    }

    function withTimeZone($tz)
    {
        $class = get_class($this);
        return new $class($this->year, $this->month, $this->day, $this->hour, $this->minute, $this->second, $tz);
    }

    function addYear($n = 1)
    {
        $class = get_class($this);
        $date = new $class(mktime($this->hour, $this->minute, $this->second, $this->month, $this->day, $this->year + $n));
        return $date->setTimeZone($this->tz);
    }

    function addMonth($n = 1)
    {
        $class = get_class($this);
        $date = new $class(mktime($this->hour, $this->minute, $this->second, $this->month + $n, $this->day, $this->year));
        return $date->setTimeZone($this->tz);
    }

    function addWeek($n = 1)
    {
        $class = get_class($this);
        $date = new $class(mktime($this->hour, $this->minute, $this->second, $this->month, $this->day + ($n * 7), $this->year));
        return $date->setTimeZone($this->tz);
    }

    function addDay($n = 1)
    {
        $class = get_class($this);
        $date = new $class(mktime($this->hour, $this->minute, $this->second, $this->month, $this->day + $n, $this->year));
        return $date->setTimeZone($this->tz);
    }

    function addHour($n = 1)
    {
        $class = get_class($this);
        $date = new $class(mktime($this->hour + $n, $this->minute, $this->second, $this->month, $this->day, $this->year));
        return $date->setTimeZone($this->tz);
    }

    function addMinute($n = 1)
    {
        $class = get_class($this);
        $date = new $class(mktime($this->hour, $this->minute + $n, $this->second, $this->month, $this->day, $this->year));
        return $date->setTimeZone($this->tz);
    }

    function addSecond($n = 1)
    {
        $class = get_class($this);
        $date = new $class(mktime($this->hour, $this->minute, $this->second + $n, $this->month, $this->day, $this->year));
        return $date->setTimeZone($this->tz);
    }

    function stripTime()
    {
        $class = get_class($this);
        return new $class($this->year, $this->month, $this->day, 0, 0, 0, $this->tz);
    }

    function stripDate()
    {
        $class = get_class($this);
        return new $class(null, null, null, $this->hour, $this->minute, $this->second, $this->tz);
    }
}
